package de.lns.render;


import static org.lwjgl.opengl.GL20.*;

import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL40;
import org.lwjgl.opengl.GL43;

import java.io.*;

import de.lns.tick.UpdateThread;


public class ShaderProgram {
	
	private static final String SHADER_PATH = "data/shaders/";
	/* !!!!!
	 * Don't change this array without changing the switch-case in loadShader().
	 */
	/**List of all Shader types to look for*/
	private static final String[] SHADER_TYPE = {
												".vert",//Vertex Shader
												".tesc",//Tesselation Control Shader
												".tese",//Tesselation Evaluation Shader
												".geom",//Geometry Shader
												".frag",//Fragment Shader
												".comp" //Compute Shader
												};
	
	public final String name;
	public final int programPointer;
	private final int[] shaderPointer;
	protected final int f_scale;
	protected final int f_angle;
	protected final int f3_displacement;
	
	private ShaderProgram(String name, int program, int[] shaders) {
		this.name = name;
		programPointer = program;
		shaderPointer = shaders;
		f_scale = glGetUniformLocation(program, "scale");
		f_angle = glGetUniformLocation(program, "angle");
		f3_displacement = glGetUniformLocation(program, "displacement");
	}
	
	public void dispose() {
		glDeleteProgram(programPointer);
		
		for(int i=0; i<shaderPointer.length; i++){
			glDeleteShader(shaderPointer[i]);
		}
	}
	
	/**
	 * This method creates a new Shader Program. <br>
	 * To do so, it looks for all shader files, <br>
	 * that match the given name and loads them. <br>
	 * After that, it links all loaded shaders into a shader program.
	 * @param name - the name that every required shader should have.
	 * @return A ShaderProgram object, that has all matching Shaders linked to it
	 */
	protected static ShaderProgram createProgram(String name) {
		boolean[] shadersToCompile = new boolean[6];
		for(int i=0; i<6; i++){shadersToCompile[i]=false;}
		int shaderCount = 0;
		//Checks the existence of every possible shader and remembers all existing ones
		for(int i=0; i<6; i++){
			File f = new File(SHADER_PATH + name + SHADER_TYPE[i]);
			if(f.exists()){
				shadersToCompile[i] = true;
				shaderCount++;
			}
		}
		//When no appropriate shaders are found, create no program and return null.
		if(shaderCount==0){
			return null;
		}
		
		/*
		 * If there is at least one fitting shader, tell the
		 * graphics adapter to reserve the space needed.
		 */
		int program = glCreateProgram();
		int[] shaders = new int[shaderCount];
		
		/*
		 * Load all shaders into the Graphics Adapter
		 * and attach them to the program, that is being created.
		 */
		int diff = 0;
		for(int i=0; i<6; i++){
			if(shadersToCompile[i]){
				shaders[i-diff] = loadShader(name, i);
				glAttachShader(program, shaders[i-diff]);
			} else{
				diff++;
			}
		}
		
		
		glLinkProgram(program);
		glValidateProgram(program);
		if(glGetProgrami(program, GL_COMPILE_STATUS) == GL11.GL_FALSE){
			UpdateThread.kill("Error while linking program: " + name, new Exception("Graphics Adapter Linking Error"));
		}
		
		return (new ShaderProgram(name, program, shaders));
	}
	
	/**
	 * This function loads the shader specified by shaderName and type <br>
	 * from a file on the hard drive into the memory of the Graphics Adapter.
	 * 
	 * @param shaderName - The name of the file, containing the shader code.
	 * @param type - The type of shader to be created.
	 * @return The OpelGL pointer to the created Program.
	 */
	private static int loadShader(String shaderName, int type){
		int shaderType;
		/* !!!!!
		 * Don't change this switch-case without changing the SHADER_TYPE array.
		 */
		switch(type){
			case(1): shaderType = GL40.GL_TESS_CONTROL_SHADER; break;
			case(2): shaderType = GL40.GL_TESS_EVALUATION_SHADER; break;
			case(3): shaderType = GL32.GL_GEOMETRY_SHADER; break;
			case(4): shaderType = GL20.GL_FRAGMENT_SHADER; break;
			case(5): shaderType = GL43.GL_COMPUTE_SHADER; break;
			default: shaderType = GL20.GL_VERTEX_SHADER; break;
		}
		int shader = glCreateShader(shaderType);
		StringBuilder shaderSource = new StringBuilder();
		
		//Read the shader code from its file.
		try{
			BufferedReader reader = new BufferedReader(
					new FileReader(SHADER_PATH + shaderName + SHADER_TYPE[type]));
			String line;
			while ((line = reader.readLine()) != null) {
				shaderSource.append(line).append("\n");
			}
			reader.close();
		} catch(IOException e) {
			UpdateThread.kill("Error while loading Shader: " + shaderName + SHADER_TYPE[type], e);
		}
		
		//Load the shader code into the Graphics Adapter's memory.
		glShaderSource(shader, shaderSource);
		//Let the Graphics Adapter compile the loaded code.
		glCompileShader(shader);
		
		/*
		 * Check, whether there were any problems,
		 * during the compilation of the shader code.
		 */
		if(glGetShaderi(shader, GL_COMPILE_STATUS) == GL11.GL_FALSE){
			String log = glGetShaderInfoLog(shader, 2000);
			UpdateThread.kill("Error while compiling Shader: " + shaderName + SHADER_TYPE[type] + "\n" + log, new Exception("Graphics Adapter Compilation Error"));
		}
		
		return shader;
	}

}
